package com.aotain.oframework.dao.sqlt;

import java.io.File;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.ParseException;
import org.apache.velocity.runtime.parser.node.SimpleNode;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Log4jConfigurer;

import com.aotain.oframework.exception.SqlTemplateInitException;
import com.aotain.oframework.exception.SqlTemplateParseException;
import com.aotain.oframework.util.LRUMap;
import com.aotain.oframework.util.io.WildCardFilter;
import com.pccw.oframework.dao.sqlt.SqltUtil;

/**
 * SQLT is an enhancement of JPA. We provide following features: 1. Use velocity
 * as SQL/JPQL parser 2. Automatically fill in query parameters 3. provide
 * utility for Velocity use.
 * @author Jacob.cw.Zhang@gmail.com
 */
public class SqlTemplateEngine {

	private static Map<String, String> jpqlCache;
	private static Map<String, String> sqlCache;
	private static final Logger logger = LoggerFactory.getLogger(SqlTemplateEngine.class);
	private static boolean debugMode = false;
	private static Object mutex = new Object();
	private static SqlTemplateEngine instance;

	public Map<String, String> getJpqlcache() {
		return jpqlCache;
	}

	public Map<String, String> getSqlcache() {
		return sqlCache;
	}

	private SqlTemplateEngine() {
		super();
	}

	public static SqlTemplateEngine getInstance() throws SqlTemplateInitException {

		return getInstance(System.getProperty("csss2.config.path") + "/sqlTemplates/");
	}

	public synchronized static SqlTemplateEngine getInstance(String sqltFolder) throws SqlTemplateInitException {
		synchronized (mutex) {
			// if (instance != null) {
			if (false) {
				if (logger.isTraceEnabled()) {
					logger.trace("returning singleton SqlTemplateEngine.");
				}
				return instance;
			} else {
				logger.info("Initiating SqlTemplateEngine.");
				instance = new SqlTemplateEngine();
				File dir = new File(sqltFolder);
				logger.debug("SQL template folder:{}", sqltFolder);
				if (dir.isDirectory()) {
					File[] files = dir.listFiles(new WildCardFilter("sqlt-*.xml"));
					if (debugMode) {
						logger.debug("getting sqlt in debug mode.");
						Map m1 = new LRUMap<String, String>(1);
						Map m2 = new LRUMap<String, String>(1);
						jpqlCache = LazyMap.decorate(m1, new SqltTransformer(files, "jpqls"));
						sqlCache = LazyMap.decorate(m2, new SqltTransformer(files, "sqls"));
					} else {
						logger.debug("getting sqlt in production mode.");
						jpqlCache = new HashMap<String, String>();
						sqlCache = new HashMap<String, String>();
						for (File file : files) {
							if (file.getName().endsWith(".xml") && file.getName().startsWith("sqlt-")) {
								logger.info("Resolving sqlt xml file {}", file.getName());
								try {
									Document tdoc = instance.read(file);
									instance.cacheTsqls(tdoc);
								} catch (MalformedURLException e) {
									throw new SqlTemplateInitException("SQL template initiation error!", e);
								} catch (DocumentException e) {
									throw new SqlTemplateInitException("SQL template initiation error!", e);
								}
							} else {
								logger.info("File {} will be ignored by sqlt-engine, please make sure sqlt file starts with 'sqlt-' and end with '.xml'.", file.getName());
							}
						}
					}
				} else {
					throw new SqlTemplateInitException("SQL template folder not found!");
				}
				logger.info("SqlTemplateEngine initialization completed.");
				return instance;
			}
		}
	}

	/**
	 * Read Document from a XML file.
	 * 
	 * @param file
	 * @return XML document object
	 * @throws MalformedURLException
	 * @throws DocumentException
	 */
	private Document read(File file) throws MalformedURLException, DocumentException {
		SAXReader reader = new SAXReader();
		Document document = reader.read(file);
		return document;
	}

	private void cacheTsqls(Document tdoc) {
		Element root = tdoc.getRootElement();
		String namespace = root.elementTextTrim("namespace");
		logger.info("Start resolving namespace {}", namespace);
		Element element = root.element("jpqls");
		if (element != null) {
			List<Element> jpqlElements = element.elements("sqlt");
			for (Element e : jpqlElements) {
				String key = namespace + "_" + e.attributeValue("id");
				String text = e.getText();
				logger.trace("Adding jpql into cache: {}: {}", key, text);
				jpqlCache.put(key, text);
			}
		} else {
			logger.info("No jpql found under namespace {}", namespace);
		}
		Element element2 = root.element("sqls");
		if (element2 != null) {
			List<Element> sqlElements = element2.elements("sqlt");
			for (Element e : sqlElements) {
				String key = namespace + "_" + e.attributeValue("id");
				String text = e.getText();
				logger.trace("Adding native sql into cache: {}: {}", key, text);
				sqlCache.put(key, text);
			}
		} else {
			logger.info("No native sql found under namespace {}", namespace);
		}

		logger.info("Completed resolving tsql xml {}", namespace);
	}

	public String mergeStringTemplateIntoString(String stringTemplate, Map model) throws SqlTemplateParseException {
		try {
			String sqlt = mergeVelocityStringTemplateIntoString(stringTemplate, model);
			sqlt = StringUtils.replace(sqlt, "\n", " ");
			sqlt = StringUtils.remove(sqlt, "\t");
			sqlt = StringUtils.trimToEmpty(sqlt);
			if (logger.isDebugEnabled()) {
				logger.debug("Merged sql template : {}", sqlt);
			}
			return sqlt;
		} catch (ParseException e) {
			throw new SqlTemplateParseException(
					"Sql Template can not be parsed.", e);
		}
	}

	public static void initVelocity() {
		if (!RuntimeSingleton.isInitialized()) {
			synchronized (mutex) {
				try {
					RuntimeSingleton.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8");
					RuntimeSingleton.setProperty(RuntimeConstants.OUTPUT_ENCODING, "UTF-8");
					RuntimeSingleton.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, SLF4JLogChute.class.getName());
					RuntimeSingleton.setProperty("runtime.log.logsystem.slf4j.logger", SqlTemplateEngine.class.getName());
					RuntimeSingleton.init();
				} catch (Exception e) {
					logger.error("Initialize Velocity Failed.", e);
				}
			}
		}
	}

	public static String mergeVelocityStringTemplateIntoString(String stringTemplate, Map model) throws ParseException {
		initVelocity();
		RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
		StringReader reader = new StringReader(stringTemplate);
		SimpleNode node = runtimeServices.parse(reader, "Template name");
		Template template = new Template();
		template.setRuntimeServices(runtimeServices);
		template.setData(node);
		template.initDocument();
		StringWriter writer = new StringWriter();
		VelocityContext velocityContext = new VelocityContext(model);
		template.merge(velocityContext, writer);
		return writer.toString();
	}

	public String getJpql(String namespace, String sqltId, Object criteria) throws SqlTemplateParseException {
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("criteria", criteria);
		model.put("util", new SqltUtil());
		String jpqlTemplate = getJpqlTemplate(namespace, sqltId);
		if (StringUtils.isBlank(jpqlTemplate)) {
			throw new SqlTemplateParseException("Sqlt-JPQL named " + sqltId + " is not found under namespace " + namespace + " .");
		}
		return mergeStringTemplateIntoString(jpqlTemplate, model);
	}

	public String getNativeSql(String namespace, String sqltId, Object criteria) throws SqlTemplateParseException {
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("criteria", criteria);
		model.put("util", new SqltUtil());
		String nativeSqlTemplate = getNativeSqlTemplate(namespace, sqltId);
		if (StringUtils.isBlank(nativeSqlTemplate)) {
			throw new SqlTemplateParseException("Sqlt-SQL named " + sqltId + " is not found under namespace " + namespace + " .");
		}
		return mergeStringTemplateIntoString(nativeSqlTemplate, model);
	}

	public String getJpqlTemplate(String namespace, String sqltId) {
		Map<String, String> j = getJpqlcache();
		return j.get(namespace + "_" + sqltId);
	}

	public String getNativeSqlTemplate(String namespace, String sqltId) {
		return getSqlcache().get(namespace + "_" + sqltId);
	}

	public static final void main(String[] args) throws Exception {
		Log4jConfigurer.initLogging("D:/CSSS2/svn/source/trunk/pom.swd.config/config/environment/dev/log4j-web.properties");
		String sqltFolder = "D:/CSSS2/svn/source/trunk/pom.swd.config/config/sqlTemplates/";
		SqlTemplateEngine engine = SqlTemplateEngine.getInstance(sqltFolder);
		engine = SqlTemplateEngine.getInstance(sqltFolder);
		engine = SqlTemplateEngine.getInstance(sqltFolder);
		engine = SqlTemplateEngine.getInstance(sqltFolder);
		for (Entry<String, String> entry : engine.getJpqlcache().entrySet()) {
			String key = entry.getKey();
			String stringTemplate = entry.getValue();
			logger.trace(" {}: {}", key, stringTemplate);
			Map<String, Object> model = new HashMap<String, Object>();
			model.put("bool1", true);
			model.put("bool2", false);
			String out = engine.mergeStringTemplateIntoString(stringTemplate, model);
			logger.debug(out);
		}

	}

}
